<%#
 Copyright 2013-2020 the original author or authors from the JHipster project.

 This file is part of the JHipster project, see https://www.jhipster.tech/
 for more information.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-%>
package <%= packageName %>.repository;

import java.util.function.BiFunction;
<%_ if (fieldsContainBigDecimal === true) { _%>
import java.math.BigDecimal;
<%_} if (fieldsContainInstant === true) { _%>
import java.time.Instant;
<%_ } if (fieldsContainLocalDate === true) { _%>
import java.time.LocalDate;
<%_ } if (fieldsContainZonedDateTime === true) { _%>
import java.time.ZonedDateTime;
<%_ } if (fieldsContainDuration === true) { _%>
import java.time.Duration;
<%_ } if (fieldsContainUUID === true) { _%>
import java.util.UUID;
<%_ } _%>
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.springframework.data.domain.Pageable;
<%_ if (databaseType === 'sql') { _%>
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;

import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.DatabaseClient.GenericInsertSpec;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.core.RowsFetchSpec;
import org.springframework.data.r2dbc.mapping.OutboundRow;
import org.springframework.data.r2dbc.mapping.SettableValue;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoin<% if (eagerRelations.length > 0) { %>Condition<% } %>;
import org.springframework.data.relational.core.sql.Table;
<%_ } _%>

import <%= packageName %>.domain.<%= asEntity(entityClass) %>;
<% relationships.forEach(function(rel) {
    if (rel.relationshipType === 'many-to-many' && rel.ownerSide) { _%>
import <%= packageName %>.domain.<%= asEntity(rel.otherEntityNameCapitalized) %>;
    <%_ } _%>
<%_ } ); _%>
<%_ Object.keys(uniqueEnums).forEach(function(element) { _%>

import <%= packageName %>.domain.enumeration.<%= element %>;
<%_ }); _%>

<%_ uniqueEntityTypes.forEach(function(element) { _%>
import <%= packageName %>.repository.rowmapper.<%= element %>RowMapper;
<%_ }); _%>
import <%= packageName %>.service.EntityManager;
<%_ if (fieldsContainOwnerManyToMany) { _%>
import <%= packageName %>.service.EntityManager.LinkTable;
<%_ } _%>

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * Spring Data <%= officialDatabaseType %> reactive custom repository implementation for the <%= asEntity(entityClass) %> entity.
 */
@SuppressWarnings("unused")
class <%= entityClass %>RepositoryInternalImpl implements <%= entityClass %>RepositoryInternal {
    private final DatabaseClient db;
    private final ReactiveDataAccessStrategy dataAccessStrategy;
    private final EntityManager entityManager;

<%_ uniqueEntityTypes.forEach(function(element) { _%>
    private final <%= element %>RowMapper <%= element.toLowerCase() %>Mapper;
<%_ }); _%>

    private final static Table entityTable = Table.aliased("<%= entityTableName %>", EntityManager.ENTITY_ALIAS);
        <%_ eagerRelations.forEach(function(rel) { _%>
    private final static Table <%= rel.relationshipName %>Table = Table.aliased("<%= rel.otherEntityTableName %>", "<%= _generateSqlSafeName(rel.relationshipName) %>");
        <%_ }); _%>

        <%_ relationships.forEach(function(rel) {
            if (rel.relationshipType === 'many-to-many' && rel.ownerSide) {
                const joinTableName = getJoinTableName(entityTableName, rel.relationshipName, prodDatabaseType);
        _%>
    private final static EntityManager.LinkTable <%= rel.relationshipName %>Link = new LinkTable("<%= joinTableName %>", "<%= getColumnName(name) %>_id", "<%= getColumnName(rel.relationshipName) %>_id");
        <%_ }
        }); _%>

    public <%= entityClass %>RepositoryInternalImpl(DatabaseClient db, ReactiveDataAccessStrategy dataAccessStrategy, EntityManager entityManager<%_ 
            uniqueEntityTypes.forEach(function(element) { _%>, <%= element %>RowMapper <%= element.toLowerCase() %>Mapper<%_ }); _%>) {
        this.db = db;
        this.dataAccessStrategy = dataAccessStrategy;
        this.entityManager = entityManager;
        <%_ uniqueEntityTypes.forEach(function(element) { _%>
        this.<%= element.toLowerCase() %>Mapper = <%= element.toLowerCase() %>Mapper;
        <%_ }); _%>
    }

    @Override
    public Flux<<%= asEntity(entityClass) %>> findAllBy(Pageable pageable) {
        return findAllBy(pageable, null);
    }

    @Override
    public Flux<<%= asEntity(entityClass) %>> findAllBy(Pageable pageable, Criteria criteria) {
        return createQuery(pageable, criteria).all();
    }

    RowsFetchSpec<<%= asEntity(entityClass) %>> createQuery(Pageable pageable, Criteria criteria) {
        List<Expression> columns = <%= asEntity(entityClass) %>SqlHelper.getColumns(entityTable, EntityManager.ENTITY_ALIAS);
        <%_ eagerRelations.forEach(function(rel) { _%>
        columns.addAll(<%= rel.otherEntityNameCapitalized %>SqlHelper.getColumns(<%= rel.relationshipName %>Table, "<%= rel.relationshipName %>"));
        <%_ }); _%>
        SelectFromAndJoin<% if (eagerRelations.length > 0) { %>Condition<% } %> selectFrom = Select.builder().select(columns).from(entityTable)<%_ eagerRelations.forEach(function(rel) { 
            const colName = _getJoinColumnName(rel); %>
            .leftOuterJoin(<%= rel.relationshipName %>Table).on(Column.create("<%= colName %>", entityTable)).equals(Column.create("id", <%= rel.relationshipName %>Table ))<%_ }); _%>;

        String select = entityManager.createSelect(selectFrom, <%= asEntity(entityClass) %>.class, pageable/*, criteria */);
        return db.execute(select)
                .map(this::process);
    }

    @Override
    public Flux<<%= asEntity(entityClass) %>> findAll() {
        return findAllBy(null, null);
    }

    @Override
    public Mono<<%= asEntity(entityClass) %>> findById(<%= primaryKeyType %> id) {
        return createQuery(null, Criteria.where("id").is(id)).one();
    }

    <%_ if (fieldsContainOwnerManyToMany) { _%>

    @Override
    public Mono<<%= asEntity(entityClass) %>> findOneWithEagerRelationships(Long id) {
        return findById(id);
    }

    @Override
    public Flux<<%= asEntity(entityClass) %>> findAllWithEagerRelationships() {
        return findAll();
    }

    @Override
    public Flux<<%= asEntity(entityClass) %>> findAllWithEagerRelationships(Pageable page) {
        return findAllBy(page);
    }

    <%_ } _%>
    private <%= asEntity(entityClass) %> process(Row row, RowMetadata metadata) {
        <%= asEntity(entityClass) %> entity = <%= entityClass.toLowerCase() %>Mapper.apply(row, "e");
        <%_ eagerRelations.forEach(function(rel) { _%>
        entity.set<%= rel.relationshipNameCapitalized %>(<%= rel.otherEntityNameCapitalized.toLowerCase() %>Mapper.apply(row, "<%= rel.relationshipName %>"));
        <%_ }); _%>
        return entity;
    }

    @Override
    public <S extends <%= asEntity(entityClass) %>> Mono<S> insert(S entity) {
        return entityManager.insert(entity);
    }

    @Override
    public <S extends <%= asEntity(entityClass) %>> Mono<S> save(S entity) {
        if (entity.getId() == null) {
            <%_ if (isUsingMapsId) { _%>
            entity.setId(entity.get<%= mapsIdAssoc.relationshipNameCapitalized %>().getId());
            <%_ } _%>
            return insert(entity)<% if (fieldsContainOwnerManyToMany) { %>.flatMap(savedEntity -> updateRelations(savedEntity))<% } %>;
        } else {
            return update(entity).map(numberOfUpdates -> {
                if (numberOfUpdates.intValue() <= 0) {
                    throw new IllegalStateException("Unable to update <%= entityClass %> with id = " + entity.getId());
                }
                return entity;
            })<% if (fieldsContainOwnerManyToMany) { %>.then(updateRelations(entity))<% } %>;
        }
    }

    @Override
    public Mono<Integer> update(<%= asEntity(entityClass) %> entity) {
        <%_ if (fields.length + eagerRelations.length > 0) { _%>
        return db.update().table(<%= asEntity(entityClass) %>.class).using(entity).fetch().rowsUpdated();
        <%_ } else { _%>
        // What can we update on this field?
        return Mono.just(1);
        <%_ } _%>
    }

    <%_ if (fieldsContainOwnerManyToMany) { _%>

    @Override
    public Mono<Void> deleteById(Long entityId) {
        return deleteRelations(entityId).then(
             db.delete().from(<%= asEntity(entityClass) %>.class).matching(Criteria.from(Criteria.where("id").is(entityId))).then()
        );
    }

    protected <S extends <%= asEntity(entityClass) %>> Mono<S> updateRelations(S entity) {
        <%_ relationships.filter(function(rel) {
                return (rel.relationshipType === 'many-to-many' && rel.ownerSide);
            }).forEach(function(rel, idx) {
                if (idx === 0) { _%>
        Mono<Void> result = entityManager.updateLinkTable(<%= rel.relationshipName %>Link, entity.getId(), entity.get<%= rel.relationshipNameCapitalizedPlural %>().stream().map(<%= asEntity(rel.otherEntityNameCapitalized) %>::getId)).then();
        <%_     } else { _%>
        result = result.and(entityManager.updateLinkTable(<%= rel.relationshipName %>Link, entity.getId(), entity.get<%= rel.relationshipNameCapitalizedPlural %>().stream().map(<%= asEntity(rel.otherEntityNameCapitalized) %>::getId)));
        <%_
                }
            }); _%>
        return result.thenReturn(entity);
    }

    protected Mono<Void> deleteRelations(Long entityId) {
        <%_ relationships.filter(function(rel) {
                return (rel.relationshipType === 'many-to-many' && rel.ownerSide);
            }).forEach(function(rel, idx) {
                if (idx === 0) { _%>
        return entityManager.deleteFromLinkTable(<%= rel.relationshipName %>Link, entityId)<% 
                } else { %>
                .and(entityManager.deleteFromLinkTable(<%= rel.relationshipName %>Link, entityId))<% } 
            }); %>;
    }

    <%_ } _%>
}

class <%= entityClass %>SqlHelper {
    static List<Expression> getColumns(Table table, String columnPrefix) {
        List<Expression> columns = new ArrayList<>();
        columns.add(Column.aliased("id", table, columnPrefix + "_id"));
<%_ fields.forEach(function(field) { 
    let col = field.fieldNameAsDatabaseColumn;
    _%>
        columns.add(Column.aliased("<%= col %>", table, columnPrefix + "_<%= col %>"));
   <%_  if ((field.fieldType === 'byte[]' || field.fieldType === 'ByteBuffer') && field.fieldTypeBlobContent !== 'text') { _%>
        columns.add(Column.aliased("<%= col %>_content_type", table, columnPrefix + "_<%= col %>_content_type"));
   <%_  } _%>
<%_ }); _%>

<%_ regularEagerRelations.forEach(function(rel) { _%>
        columns.add(Column.aliased("<%= getColumnName(rel.relationshipName) %>_id", table, columnPrefix + "_<%= getColumnName(rel.relationshipName) %>_id"));
<%_ }); _%>
        return columns;
    }

}
